package com.example.filter;

import com.alibaba.fastjson2.JSONObject;
import com.example.utils.Const;
import com.example.utils.SnowflakeIdGenerator;
import jakarta.annotation.Resource;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.http.MediaType;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingResponseWrapper;

import java.io.IOException;
import java.util.Set;

/**
 * 请求日志过滤器，用于记录所有用户请求信息。
 * 该过滤器会对每个请求进行检查，记录请求的相关信息（如请求URL、参数、用户信息等）
 * 以及请求处理完成后的响应信息（如响应状态码、响应内容等），便于系统的监控和问题排查。
 */
@Slf4j
@Component
public class RequestLogFilter extends OncePerRequestFilter {

    // 注入SnowflakeIdGenerator对象，用于生成唯一的请求ID
    @Resource
    SnowflakeIdGenerator generator;

    // 定义一个Set集合，包含不需要记录日志的URL前缀，这里是"/swagger-ui"和"/v3/api-docs"
    private final Set<String> ignores = Set.of("/swagger-ui", "/v3/api-docs");

    /**
     * 执行过滤操作的核心方法，该方法会在每个请求经过过滤器时被调用一次。
     *
     * @param request 用于获取请求信息的HttpServletRequest对象，包含了请求的各种属性和参数。
     * @param response 用于获取响应信息的HttpServletResponse对象，用于设置响应的状态码、头信息和内容等。
     * @param filterChain 过滤链，用于将请求传递给下一个过滤器或目标资源，以完成整个请求处理流程。
     *
     * 此方法首先判断请求的URL是否在忽略列表中，如果在，则直接传递请求到下一个过滤器或目标资源；
     * 如果不在忽略列表中，则记录请求开始的时间，并使用ContentCachingResponseWrapper包装响应对象，
     *这样可以缓存响应内容，以便之后记录日志使用。在请求处理完成后，记录请求结束的日志，并将缓存的内容复制到实际响应中。
     *
     * 注意：此方法重写了父类的doFilterInternal方法，以实现自定义的过滤逻辑。
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 判断请求URL是否需要被忽略
        if (this.isIgnoreUrl(request.getServletPath())) {
            // 如果需要忽略，则直接将请求传递给过滤链中的下一个过滤器或目标资源
            filterChain.doFilter(request, response);
        } else {
            // 记录请求开始的时间，用于计算请求处理的耗时
            long startTime = System.currentTimeMillis();
            // 调用logRequestStart方法记录请求开始的相关信息
            this.logRequestStart(request);
            // 使用ContentCachingResponseWrapper包装响应对象，以便缓存响应内容
            // 这样在记录日志时可以获取到完整的响应信息
            ContentCachingResponseWrapper wrapper = new ContentCachingResponseWrapper(response);
            // 将请求传递给过滤链中的下一个过滤器或目标资源，并使用包装过的响应对象
            filterChain.doFilter(request, wrapper);
            // 调用logRequestEnd方法记录请求结束的相关信息，包括响应状态码、内容和处理耗时等
            this.logRequestEnd(wrapper, startTime);
            // 将缓存的响应内容复制到实际的响应中，确保客户端能够收到完整的响应
            wrapper.copyBodyToResponse();
        }
    }

    /**
     * 判定当前请求url是否不需要日志打印。
     * 此方法用于检查传入的URL是否在预定义的忽略列表中，如果URL以忽略列表中的任何一个前缀开始，
     * 则认为该URL被忽略，主要用于减少不必要的日志输出，提高系统性能。
     *
     * @param url 待检查的请求路径
     * @return 如果URL被忽略返回true，否则返回false
     */
    private boolean isIgnoreUrl(String url) {
        // 遍历忽略URL列表
        for (String ignore : ignores) {
            // 如果当前URL以忽略前缀开始，则返回true，表示该URL被忽略
            if (url.startsWith(ignore)) return true;
        }
        // 如果没有找到匹配的忽略前缀，返回false，表示该URL不被忽略
        return false;
    }

    /**
     * 记录请求结束时的信息，包括请求处理耗时、响应状态码和响应内容等。
     *
     * @param wrapper 包装了响应内容的ContentCachingResponseWrapper对象，用于获取响应内容和状态。
     * @param startTime 请求开始的时间戳，用于计算请求处理耗时。
     */
    public void logRequestEnd(ContentCachingResponseWrapper wrapper, long startTime) {
        // 计算请求处理耗时，通过当前时间减去请求开始时间得到
        long time = System.currentTimeMillis() - startTime;
        // 获取响应状态码，用于判断请求处理是否成功
        int status = wrapper.getStatus();
        // 获取响应内容类型，用于确定如何处理响应内容
        String contentType = wrapper.getContentType();
        String content;

        // 根据响应状态码和内容类型，选择合适的处理方式来获取响应内容
        if (status != 200) {
            // 如果状态码不是200，说明请求处理过程中出现了错误，记录错误信息
            content = status + " 错误";
        } else if (contentType != null && (contentType.startsWith(MediaType.TEXT_PLAIN_VALUE) ||
                contentType.startsWith(MediaType.APPLICATION_JSON_VALUE) ||
                contentType.startsWith(MediaType.APPLICATION_XML_VALUE))) {
            // 如果内容类型是文本、JSON或XML，将响应内容作为字符串获取
            content = new String(wrapper.getContentAsByteArray());
        } else {
            // 如果内容类型是二进制数据，记录数据的大小
            content = "二进制数据，大小: " + wrapper.getContentSize() + " 字节";
        }
        // 记录请求处理耗时和响应结果，便于分析请求的性能和处理情况
        log.info("请求处理耗时: {}ms | 响应结果: {}", time, content);
    }

    /**
     * 请求开始时的日志打印，包含请求全部信息，以及对应用户角色。
     *
     * @param request 请求对象，用于获取请求的相关信息。
     */
    public void logRequestStart(HttpServletRequest request) {
        // 生成请求ID，使用SnowflakeIdGenerator生成唯一的ID
        long reqId = generator.nextId();
        // 将请求ID放入日志上下文中，方便在日志中追踪该请求
        MDC.put("reqId", String.valueOf(reqId));
        // 创建JSON对象用于存储请求参数，方便以结构化的方式记录参数信息
        JSONObject object = new JSONObject();
        // 遍历请求参数并存入JSON对象，只取参数数组的第一个值（如果有多个值的情况）
        request.getParameterMap().forEach((k, v) -> object.put(k, v.length > 0? v[0] : null));
        // 获取用户ID，从请求的属性中获取用户ID信息
        Object id = request.getAttribute(Const.ATTR_USER_ID);
        if (id != null) {
            // 获取当前用户信息，从安全上下文持有者中获取认证的用户信息
            User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
            // 打印包含用户信息的日志，包括请求URL、请求方法、远程IP地址、用户身份、用户ID、用户角色和请求参数列表
            log.info("请求URL: \"{}\" ({}) | 远程IP地址: {} │ 身份: {} (UID: {}) | 角色: {} | 请求参数列表: {}",
                    request.getServletPath(), request.getMethod(), request.getRemoteAddr(),
                    user.getUsername(), id, user.getAuthorities(), object);
        } else {
            // 打印不包含用户信息的日志，说明用户未经过认证，只记录请求URL、请求方法、远程IP地址和请求参数列表
            log.info("请求URL: \"{}\" ({}) | 远程IP地址: {} │ 身份: 未验证 | 请求参数列表: {}",
                    request.getServletPath(), request.getMethod(), request.getRemoteAddr(), object);
        }
    }
}